Relay 是一種使用 GraphQL 的規範,其名稱來自於由 Facebook 開發的 JavaScript 框架 Relay。
這種規範規定了一些特定的慣用格式,是讓客戶端能更加高效、易於使用與 GraphQL 伺服器連結。
Relay 也是 GraphQL 官方教學中最佳實踐的內容,它對於伺服器端開發,主要有三個重點:
id
欄位,通常id
欄位會使用全域唯一 ID 型態,這種全域物件識別(Global Object Identification)的作法,統一物件查詢的方式,也有助於客戶端快取查詢結果與物件 [2]。接下來我們先將物件改成 Relay 風格的格式,下面先將我們定義型態改成繼承節點介面,id
欄位使用全域唯一 ID:
# server/app/blog/graph/types.py
from django.db.models import QuerySet
from strawberry.types import Info
+from strawberry import relay
from server.app.authentication.graph import types as auth_types
# ... 省略
@strawberry_django.type(blog_models.Post)
-class Post:
- id: uuid.UUID # noqa: A003
+class Post(relay.Node):
+ id: relay.NodeID[uuid.UUID] # noqa: A003
# ... 省略
# ... 省略
@strawberry_django.type(blog_models.Comment)
-class Comment:
- id: uuid.UUID # noqa: A003
+class Comment(relay.Node):
+ id: relay.NodeID[uuid.UUID] # noqa: A003
# ... 省略
# ... 省略
-class Tag:
+class Tag(relay.Node):
+ id: relay.NodeID[uuid.UUID] # noqa: A003
# ... 省略
# ... 省略
@strawberry_django.type(blog_models.Category)
-class Category:
- id: uuid.UUID # noqa: A003
+class Category(relay.Node):
+ id: relay.NodeID[uuid.UUID] # noqa: A003
# ... 省略
# ... 省略
@strawberry_django.input(blog_models.Post)
class PostInput:
slug: str
title: str
content: str
tags: list[relay.GlobalID]
categories: list[relay.GlobalID]
author: str = strawberry.field(description="Username of the author")
- tags: list[str] = strawberry.field(description="List of tag names")
- categories: list[str] = strawberry.field(description="List of category slugs")
# ... 省略
@strawberry_django.partial(blog_models.Post)
class PostInputPartial:
- id: strawberry.auto # noqa: A003
+ id: relay.GlobalID # noqa: A003
slug: strawberry.auto
title: strawberry.auto
content: strawberry.auto
- tags: strawberry.auto
- categories: strawberry.auto
+ tags: list[relay.GlobalID] | None
+ categories: list[relay.GlobalID] | None
published_at: datetime.datetime | None
published: bool | None
-@strawberry_django.input(blog_models.Post)
-class PostIdInput:
- id: strawberry.auto # noqa: A003
繼承relay.Node
的型態必須定義ㄧ個id
欄位,或是將某個欄位的型態設為relay.NodeID[T]
,
而定義型態為relay.NodeID[T]
的欄位會在 Schema 被轉成relay.GlobalID
型態。
修改完型態後,就可以試著做查詢:
query MyQuery {
posts {
id
title
slug
publishedAt
published
content
tags {
id
}
categories {
id
}
}
}
relay.GlobalID
型態的值會是 base64 編碼的字串,解開會是Type:ID
的形式。
$ echo "VGFnOjI3YzE4ZGQwLTg5MDUtNGU5Yy1iMzY4LTZmMTg1NmNkNzA0NA==" | base64 -d
Tag:27c18dd0-8905-4e9c-b368-6f1856cd7044
接著修改變更:
# server/app/blog/graph/mutations.py
# ... 省略
from django.utils import timezone
from strawberry.file_uploads import Upload
+from strawberry.types import Info
from strawberry.utils.str_converters import to_camel_case
from strawberry_django import mutations
# ... 省略
@strawberry.type
class Mutation:
- update_post: blog_types.Post = mutations.update(blog_types.PostInputPartial)
- delete_post: blog_types.Post = mutations.delete(blog_types.PostIdInput)
+ @strawberry_django.mutation(handle_django_errors=True)
+ def update_post(
+ self,
+ data: blog_types.PostInputPartial,
+ info: Info,
+ ) -> blog_types.Post:
+ post = data.id.resolve_node_sync(info, ensure_type=blog_models.Post)
+ input_data = vars(data)
+ for field, value in input_data.items():
+ if field in ("id", "tags", "categories"):
+ continue
+
+ if value and hasattr(post, field):
+ setattr(post, field, value)
+
+ post.save()
+ if data.tags is not None:
+ tags = [
+ tag_id.resolve_node_sync(info, ensure_type=blog_models.Tag)
+ for tag_id in data.tags
+ ]
+ post.tags.set(tags)
+
+ if data.categories is not None:
+ categories = [
+ category_id.resolve_node_sync(info, ensure_type=blog_models.Category)
+ for category_id in data.categories
+ ]
+ post.categories.set(categories)
+
+ return typing.cast(blog_types.Post, post)
# ... 省略
這邊將使用 strawberry_django 內建變更功能移除,重新實作一個更新文章的變更功能,前面查詢的地方所有的id
欄位都被定義成relay.GlobalID
型態,所以在變更輸入資料也是使用relay.GlobalID
型態的id
欄位,relay.GlobalID
會是一個 base64 編碼的字串,relay.GlobalID
裡面有個resolve_node
的方法能夠將它轉成 Django 的模型物件,然後resolve_node
是非同步的方法,同步的方法是使用resolve_node_sync
,有個ensure_type
參數是帶入要轉成 Django 的模型類別。
修改完後,就可以試著執行更新文章:
mutation MyMutation($data: PostInputPartial!) {
updatePost(data: $data) {
... on Post {
content
id
published
publishedAt
slug
title
categories {
id
slug
}
tags {
id
name
}
}
}
}
再我們將原本的查詢做修改,分頁改成連接模式的分頁:
# server/app/blog/graph/queries.py
import strawberry
import strawberry_django
+from strawberry import relay
# ... 省略
@strawberry.type
class Query:
- posts: list[blog_types.Post] = strawberry_django.field(
+ posts: relay.ListConnection[blog_types.Post] = strawberry_django.connection(
order=blog_orders.PostOrder,
- pagination=True,
filters=blog_filters.PostFilter,
)
query MyQuery {
posts(
after: "YXJyYXljb25uZWN0aW9uOjA=",
before: "",
first: 1,
last: null
) {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
edges {
cursor
node {
content
id
title
}
}
}
}
after
和before
:這些參數用於查詢的分頁。after
指定了查詢結果的起始點,系統將回傳此指標之後的資料,before
則是用於設置查詢結果的結束點,系統將返回此指標之前的資料。first
和last
:這些參數用於設定限制查詢的筆數。如果只想取前 n 個結果,就使用first
,如果想取最後 n 個結果,使用last
。pageInfo
:這是連接模式的查詢重要部分,它包含了有關查詢結果的有用資訊。
endCursor
:這是最後一筆資料的指標。如果希望獲得更多的後續資料,可以使用after
參數並提供此endCursor
值。hasNextPage
:這是一個 bool 型態,表示是否還有更多的資料可以查詢。如果為真,表示可以使用after
和endCursor
繼續查詢更多資料。hasPreviousPage
:這是一個 bool 型態,表示是否還有前面的資料可以查詢。如果為真,表示可以使用before
和startCursor
繼續查詢更多資料。startCursor
:這是第一筆資料的指標。如果你想獲得更多的前面的資料,可以使用before
參數並提供此startCursor
值。edges
:這是連接查詢結果的一部分,包括了指標和節點(node)。
cursor
:每個邊(edge)都有一個指標,表示其在連接的位置。在執行分頁查詢時,cursor
是非常有用的。node
:這是一個包含了實際資料的物件。strawberry_django 提供另一個連接模式的分頁型態,它多了顯示總筆數的欄位:
# server/app/blog/graph/queries.py
import strawberry
import strawberry_django
-from strawberry import relay
+from strawberry_django.relay import ListConnectionWithTotalCount
from server.app.blog.graph import filters as blog_filters
from server.app.blog.graph import orders as blog_orders
# ... 省略
@strawberry.type
class Query:
- posts: relay.ListConnection[blog_types.Post] = strawberry_django.connection(
+ posts: ListConnectionWithTotalCount[blog_types.Post] = strawberry_django.connection(
order=blog_orders.PostOrder,
filters=blog_filters.PostFilter,
)
query MyQuery {
posts(
after: "YXJyYXljb25uZWN0aW9uOjA=",
before: "",
first: 1,
last: null
) {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
edges {
cursor
node {
content
id
title
}
}
totalCount
}
}
這次修改內容可以參考 Git commit: